Popular Searches
Popular Course Categories
Popular Courses

Building Lists Using LazyColumn and LazyRow

Building Lists Using LazyColumn and LazyRow

Modern Android UI with Jetpack Compose

Building Lists Using LazyColumn and LazyRow in Jetpack Compose

Course: Android App Development

Topic: Building Lists Using LazyColumn and LazyRow

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction to Lists in Android Development

Lists are one of the most frequently used components in Android applications. Many apps need to display collections of data, such as contacts, messages, products, notifications, songs, news articles, and transaction histories. A well-designed list allows users to browse information, interact with individual entries, and navigate through large amounts of content efficiently.

Jetpack Compose provides two important components for building scrolling lists: LazyColumn and LazyRow. Both are designed to display collections efficiently by composing and laying out items as they are needed for the visible viewport, rather than creating every item in the collection at once.

LazyColumn displays items vertically, while LazyRow displays items horizontally. They are useful for both small interactive collections and large data-driven screens.

2. Learning Objectives

After studying this topic, you should be able to:

  • Understand lazy layouts and why they are useful in Jetpack Compose.
  • Differentiate between LazyColumn, LazyRow, Column, and Row.
  • Create lists from Kotlin collections using the items() function.
  • Add individual elements using item().
  • Access item positions using itemsIndexed().
  • Design reusable list-item composables.
  • Configure spacing, padding, alignment, and scrolling.
  • Use stable item keys and content types where appropriate.
  • Manage list state and implement programmatic scrolling.
  • Build practical lists for products, contacts, messages, and categories.
  • Identify common performance problems and avoid incorrect nested scrolling.

3. What Is a Lazy List?

A lazy list is a scrolling layout that creates and lays out items as they are needed for display. It does not normally compose the entire collection at the beginning. As the user scrolls, Compose prepares additional items when they become necessary and can dispose of items that are no longer needed.

The word lazy refers to this on-demand behavior. It does not mean that the interface is slow. The purpose is to avoid unnecessary work when displaying large or dynamically changing collections.

Example of a lazy list

LazyColumn {
    items(1000) { index ->
        Text(text = "Item $index")
    }
}

This example defines 1,000 entries without requiring all 1,000 item composables to be composed and laid out immediately.

Why use lazy lists?

  • Efficiency: Only the items needed around the visible area are composed and laid out.
  • Scalability: They are suitable for collections that contain many records.
  • Scrolling: Vertical and horizontal scrolling are built into the components.
  • Flexible content: Each item can contain text, images, buttons, icons, or custom layouts.
  • Dynamic data: Lists can display information that changes as application state changes.
  • Reusable UI: Item layouts can be extracted into independent composable functions.

4. Understanding LazyColumn and LazyRow

Feature LazyColumn LazyRow
Direction Vertical Horizontal
Primary scrolling Up and down Left and right, according to layout direction
Typical use Contacts, messages, product lists Categories, image galleries, featured products
Spacing parameter verticalArrangement horizontalArrangement
Content padding contentPadding contentPadding
List state LazyListState LazyListState

LazyColumn syntax

LazyColumn(
    modifier = Modifier.fillMaxSize()
) {
    // Add list content here
}

LazyRow syntax

LazyRow(
    modifier = Modifier.fillMaxWidth()
) {
    // Add horizontal list content here
}

Both components use a LazyListScope content block. Within this block, you describe individual items or collections of items using functions such as item(), items(), and itemsIndexed().

5. Required Imports

The following imports are commonly needed for the examples in these notes. Android Studio can also add imports automatically when you select an unresolved symbol.

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

Some examples use additional APIs such as coroutines, images, or state management. Add the corresponding imports when using those examples.

6. Creating Your First LazyColumn

The simplest way to build a vertical list is to place text items inside a LazyColumn. The items(count) overload creates content for a specified number of entries.

@Composable
fun SimpleVerticalList() {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(20) { index ->
            Text(
                text = "List Item ${index + 1}",
                modifier = Modifier
                    .fillMaxWidth()
                    .background(
                        color = Color(0xFFE8EAF6),
                        shape = RoundedCornerShape(8.dp)
                    )
                    .padding(16.dp),
                fontSize = 18.sp
            )
        }
    }
}

Explanation

  1. LazyColumn creates a vertically scrolling container.
  2. Modifier.fillMaxSize() asks the list to occupy the available size offered by its parent.
  3. contentPadding adds space around the list content.
  4. Arrangement.spacedBy(8.dp) creates an 8 dp gap between consecutive items.
  5. items(20) defines 20 entries.
  6. The lambda receives the index of each entry, starting at zero.
  7. The displayed label uses index + 1 so that numbering begins at one.

7. Creating a LazyRow

Use LazyRow when items should appear side by side and users should scroll horizontally. It is commonly used for category chips, movie cards, product carousels, and image collections.

@Composable
fun SimpleHorizontalList() {
    LazyRow(
        modifier = Modifier.fillMaxWidth(),
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(10) { index ->
            Card(
                modifier = Modifier.size(
                    width = 140.dp,
                    height = 100.dp
                )
            ) {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "Card ${index + 1}",
                        style = MaterialTheme.typography.titleMedium
                    )
                }
            }
        }
    }
}

Explanation

  • LazyRow arranges items horizontally.
  • contentPadding adds horizontal space at the beginning and end of the content.
  • horizontalArrangement controls the gap between adjacent cards.
  • Modifier.size() gives each card a defined width and height.
  • Alignment.Center positions the card label in the center of its available content area.

8. Adding a Single Item with item()

The item() function adds one independent entry to the lazy list. It is useful for a title, introduction, banner, empty-state message, section label, or footer.

@Composable
fun ListWithHeaderAndFooter() {
    val names = listOf(
        "Aarav",
        "Priya",
        "Rohan",
        "Ananya"
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        item {
            Text(
                text = "Contact Directory",
                style = MaterialTheme.typography.headlineSmall,
                modifier = Modifier.padding(bottom = 8.dp)
            )
        }

        items(names) { name ->
            Text(
                text = name,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFF3F4F6))
                    .padding(16.dp)
            )
        }

        item {
            Text(
                text = "End of contacts",
                modifier = Modifier.padding(top = 8.dp)
            )
        }
    }
}

The header, contact entries, and footer belong to the same lazy list. This is often preferable to placing a vertically scrolling LazyColumn inside another vertically scrolling container.

9. Displaying Kotlin Collections with items()

Most real applications receive a collection of objects from local storage, a repository, or a web service. The items() extension function lets you render each object from a Kotlin list.

@Composable
fun FruitList() {
    val fruits = listOf(
        "Apple",
        "Banana",
        "Mango",
        "Orange",
        "Pineapple"
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(fruits) { fruit ->
            Text(
                text = fruit,
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFE8F5E9))
                    .padding(16.dp)
            )
        }
    }
}

Here, the lambda receives the actual fruit string rather than an integer index. This makes it convenient to render information from each collection entry.

Using a data class

For structured information, create a data class to represent an item. Data classes keep related properties together and make the UI easier to maintain.

data class Student(
    val id: Int,
    val name: String,
    val course: String
)

@Composable
fun StudentList() {
    val students = listOf(
        Student(1, "Aarav", "Android"),
        Student(2, "Priya", "Kotlin"),
        Student(3, "Rohan", "Java"),
        Student(4, "Ananya", "UI Design")
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = students,
            key = { student -> student.id }
        ) { student ->
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFF5F5F5))
                    .padding(16.dp)
            ) {
                Text(
                    text = student.name,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(text = "Course: ${student.course}")
            }
        }
    }
}

In this example, each student has an ID, a name, and a course. The ID is used as a stable key so Compose can identify each item when the list changes.

10. Understanding itemsIndexed()

The itemsIndexed() function provides both the position and the corresponding data object. Use it when the UI needs numbered entries, position-based styling, or information about an item's index.

@Composable
fun NumberedTaskList() {
    val tasks = listOf(
        "Create project",
        "Design interface",
        "Write Kotlin code",
        "Test application",
        "Publish application"
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        itemsIndexed(tasks) { index, task ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .background(Color(0xFFF1F5F9))
                    .padding(12.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "${index + 1}.",
                    style = MaterialTheme.typography.titleMedium
                )
                Spacer(modifier = Modifier.width(12.dp))
                Text(text = task)
            }
        }
    }
}

The index starts at zero, so adding one creates user-friendly numbering. If entries can be reordered or removed, avoid treating the index as the permanent identity of an item. Prefer a stable ID for identity.

11. Designing Reusable List-Item Composables

A reusable item composable separates the presentation of one entry from the code that manages the overall collection. This improves readability and makes it easier to reuse the same design in multiple screens.

data class Contact(
    val id: Int,
    val name: String,
    val phone: String
)

@Composable
fun ContactRow(contact: Contact) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(
                color = Color(0xFFF8FAFC),
                shape = RoundedCornerShape(10.dp)
            )
            .padding(16.dp)
    ) {
        Text(
            text = contact.name,
            style = MaterialTheme.typography.titleMedium
        )
        Spacer(modifier = Modifier.height(4.dp))
        Text(
            text = contact.phone,
            style = MaterialTheme.typography.bodyMedium
        )
    }
}

@Composable
fun ContactDirectory() {
    val contacts = listOf(
        Contact(101, "Aarav Sharma", "9000011111"),
        Contact(102, "Priya Patel", "9000022222"),
        Contact(103, "Rohan Mehta", "9000033333")
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(
            items = contacts,
            key = { contact -> contact.id }
        ) { contact ->
            ContactRow(contact = contact)
        }
    }
}

Benefits of reusable list items

  • Each composable has a clear responsibility.
  • Visual changes can be made in one place.
  • The list-building code becomes shorter and easier to read.
  • UI components can be tested independently.
  • Consistent layouts are easier to maintain across an application.

12. Adding Stable Keys to List Items

A key uniquely identifies a list entry. By default, lazy layouts associate item state with item position. If entries are inserted, removed, or reordered, position-based identity can cause remembered state to be associated with the wrong entry or reset unexpectedly.

LazyColumn {
    items(
        items = students,
        key = { student -> student.id }
    ) { student ->
        StudentRow(student)
    }
}

Rules for good keys

  • Every key in a list should be unique.
  • Use a stable identifier, such as a database ID or a unique server ID.
  • Avoid generating a new random key during recomposition.
  • Do not use the item's position as its identity when the collection can change order.
  • On Android, choose key types supported by saved-state mechanisms when item state must be restored through rememberSaveable.

Stable keys help Compose track items through data changes. They are especially important when list entries contain remembered UI state or when using item animations.

13. Understanding contentType

A lazy list may contain different types of entries, such as headers, product cards, advertisements, and message rows. The contentType parameter can identify which entries share the same general UI structure.

data class FeedEntry(
    val id: Int,
    val type: String,
    val title: String
)

@Composable
fun MixedFeed(entries: List<FeedEntry>) {
    LazyColumn {
        items(
            items = entries,
            key = { it.id },
            contentType = { it.type }
        ) { entry ->
            when (entry.type) {
                "header" -> Text(
                    text = entry.title,
                    style = MaterialTheme.typography.headlineSmall
                )
                "article" -> Text(
                    text = entry.title,
                    modifier = Modifier.padding(16.dp)
                )
                else -> Text(text = entry.title)
            }
        }
    }
}

Providing a meaningful content type can help Compose reuse compatible item compositions more efficiently. It is most useful when a list contains multiple distinct item structures; it is not necessary for every simple list.

14. Configuring Padding and Spacing

Padding and spacing make lists easier to read and prevent entries from appearing crowded.

Property Purpose Example
contentPadding Space around the lazy list's content PaddingValues(16.dp)
verticalArrangement Vertical arrangement and spacing in LazyColumn Arrangement.spacedBy(8.dp)
horizontalArrangement Horizontal arrangement and spacing in LazyRow Arrangement.spacedBy(12.dp)
Modifier.padding() Space inside or around an individual composable Modifier.padding(12.dp)
Modifier.fillMaxWidth() Use the available width offered by the parent Modifier.fillMaxWidth()

Example: a spaced list

LazyColumn(
    contentPadding = PaddingValues(
        horizontal = 16.dp,
        vertical = 12.dp
    ),
    verticalArrangement = Arrangement.spacedBy(10.dp)
) {
    items(products, key = { it.id }) { product ->
        ProductRow(product)
    }
}

Remember that contentPadding applies to the lazy list's content, including the beginning and end of the collection. Item padding controls the spacing within or around an individual item. Choose a consistent spacing scale so the interface looks organized.

15. Building a Horizontal Product Category List

A common shopping interface displays categories in a horizontal row. The user can swipe sideways to explore more categories without taking up the entire screen height.

@Composable
fun ProductCategories() {
    val categories = listOf(
        "All",
        "Electronics",
        "Clothing",
        "Books",
        "Home",
        "Sports"
    )

    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(categories) { category ->
            Card(
                shape = RoundedCornerShape(20.dp)
            ) {
                Text(
                    text = category,
                    modifier = Modifier.padding(
                        horizontal = 18.dp,
                        vertical = 10.dp
                    )
                )
            }
        }
    }
}

This component can be placed above a product grid or list. To make categories interactive, use a clickable component or a button and maintain the selected category in state.

16. Building a Product List with Clickable Items

Many list entries need to respond when the user taps them. For example, selecting a product may open its detail page, while tapping a contact may open a profile screen.

data class Product(
    val id: Int,
    val name: String,
    val price: String
)

@Composable
fun ProductRow(
    product: Product,
    onProductClick: (Product) -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable {
                onProductClick(product)
            },
        elevation = CardDefaults.cardElevation(
            defaultElevation = 2.dp
        )
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = product.name,
                style = MaterialTheme.typography.titleMedium
            )
            Spacer(modifier = Modifier.height(6.dp))
            Text(text = product.price)
        }
    }
}

@Composable
fun ProductList(
    onProductSelected: (Product) -> Unit
) {
    val products = listOf(
        Product(1, "Wireless Headphones", "₹1,999"),
        Product(2, "Bluetooth Speaker", "₹1,499"),
        Product(3, "Smart Watch", "₹2,999")
    )

    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(
            items = products,
            key = { it.id }
        ) { product ->
            ProductRow(
                product = product,
                onProductClick = onProductSelected
            )
        }
    }
}

The parent provides the click callback. This keeps navigation or business logic outside the reusable row composable. In a real application, the screen or navigation layer can use the selected product ID to open a details destination.

17. Managing Lazy List State

Lazy lists expose a LazyListState object that tracks scroll information and allows the application to control scrolling. Create the state with rememberLazyListState() and pass it to the list's state parameter.

@Composable
fun StatefulList() {
    val listState = rememberLazyListState()

    LazyColumn(
        state = listState,
        contentPadding = PaddingValues(16.dp)
    ) {
        items(100) { index ->
            Text(
                text = "Entry ${index + 1}",
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            )
        }
    }
}

The state can expose information such as the first visible item index and scroll offset. It can also be used to move to a particular entry.

Scrolling to an item programmatically

Functions such as scrollToItem() and animateScrollToItem() are suspending functions, so they must be called from a coroutine.

import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch

@Composable
fun ListWithScrollButton() {
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()

    Column {
        Button(
            onClick = {
                coroutineScope.launch {
                    listState.animateScrollToItem(0)
                }
            }
        ) {
            Text("Scroll to Top")
        }

        LazyColumn(
            state = listState,
            modifier = Modifier.fillMaxSize()
        ) {
            items(100) { index ->
                Text(
                    text = "Item ${index + 1}",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                )
            }
        }
    }
}

In this example, tapping the button starts a coroutine and smoothly scrolls to the first list entry. The index passed to the scrolling function is zero-based.

18. Handling Empty Lists and Loading States

Real applications often retrieve list content asynchronously. The interface should clearly distinguish between loading, an empty result, a successful result, and an error. Do not assume an empty collection always means a loading failure.

@Composable
fun TaskListScreen(
    tasks: List<String>,
    isLoading: Boolean
) {
    when {
        isLoading -> {
            Text(
                text = "Loading tasks...",
                modifier = Modifier.padding(16.dp)
            )
        }

        tasks.isEmpty() -> {
            Text(
                text = "No tasks available.",
                modifier = Modifier.padding(16.dp)
            )
        }

        else -> {
            LazyColumn(
                contentPadding = PaddingValues(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(tasks) { task ->
                    Text(
                        text = task,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(12.dp)
                    )
                }
            }
        }
    }
}

This example uses parameters to demonstrate different UI states. In a production application, the loading and data values would commonly come from a screen state or ViewModel. Error states and retry actions should also be represented when data comes from a network or database.

19. Combining LazyColumn and LazyRow

A common application design uses a vertically scrolling page with horizontal carousels inside it. For example, a media application may show a heading followed by a horizontal list of popular films and another section containing recently added content.

@Composable
fun MediaHomeScreen() {
    val featured = listOf(
        "Popular",
        "Trending",
        "Recommended",
        "New Releases"
    )

    val recent = listOf(
        "Episode 1",
        "Episode 2",
        "Episode 3",
        "Episode 4"
    )

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(vertical = 16.dp),
        verticalArrangement = Arrangement.spacedBy(20.dp)
    ) {
        item {
            Text(
                text = "Explore",
                style = MaterialTheme.typography.headlineMedium,
                modifier = Modifier.padding(horizontal = 16.dp)
            )
        }

        item {
            Text(
                text = "Featured Categories",
                style = MaterialTheme.typography.titleLarge,
                modifier = Modifier.padding(horizontal = 16.dp)
            )

            LazyRow(
                contentPadding = PaddingValues(horizontal = 16.dp),
                horizontalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                items(featured) { title ->
                    Card {
                        Text(
                            text = title,
                            modifier = Modifier.padding(18.dp)
                        )
                    }
                }
            }
        }

        item {
            Text(
                text = "Recently Added",
                style = MaterialTheme.typography.titleLarge,
                modifier = Modifier.padding(horizontal = 16.dp)
            )

            LazyRow(
                contentPadding = PaddingValues(horizontal = 16.dp),
                horizontalArrangement = Arrangement.spacedBy(10.dp)
            ) {
                items(recent) { episode ->
                    Card(
                        modifier = Modifier.size(
                            width = 150.dp,
                            height = 110.dp
                        )
                    ) {
                        Box(
                            modifier = Modifier.fillMaxSize(),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = episode)
                        }
                    }
                }
            }
        }
    }
}

The vertically scrolling parent contains separate items, and each item may contain a horizontally scrolling row. This is a useful pattern because the nested scrolling directions differ. For this example, each section is placed inside a single parent-list item so the main screen has one vertical scrolling container.

20. Important Rule for Nested Scrolling

Avoid placing an unbounded, vertically scrolling LazyColumn inside another vertically scrollable container, such as a Column with verticalScroll(). This can lead to measurement errors or unclear scrolling behavior because both containers compete for vertical space.

For a page containing a header, multiple sections, a list of records, and a footer, prefer one parent LazyColumn:

LazyColumn {
    item {
        ScreenHeader()
    }

    item {
        SectionTitle()
    }

    items(records, key = { it.id }) { record ->
        RecordRow(record)
    }

    item {
        ScreenFooter()
    }
}

Nested lazy layouts can be valid when the inner scrolling container has appropriate size constraints, and horizontal lists inside vertical lists are a common design. The key is to provide clear constraints and avoid competing unbounded scrolling in the same direction.

21. List Animations

Lazy list items can animate when they are inserted, removed, or moved. A stable key helps Compose identify which item changed position.

LazyColumn {
    items(
        items = tasks,
        key = { task -> task.id }
    ) { task ->
        Row(
            modifier = Modifier
                .animateItem()
                .fillMaxWidth()
                .padding(12.dp)
        ) {
            Text(text = task.title)
        }
    }
}

Use item animations when they improve the user's understanding of a change, such as adding a task or reordering a playlist. Avoid unnecessary animation that makes frequent updates distracting. The exact available animation API can depend on the Compose version used by the project.

22. Performance Best Practices

  • Use lazy layouts for long collections: Avoid composing a very large list in a plain Column or Row when only part of the content is visible.
  • Provide stable keys: Use unique IDs when entries may be inserted, deleted, or reordered.
  • Use content types for mixed layouts: Help Compose reuse compatible item compositions when the list has different item structures.
  • Keep item composables focused: Extract complex layouts into small reusable composables.
  • Avoid unnecessary work in item lambdas: Do not repeatedly perform expensive calculations or network requests while composing a row.
  • Use appropriate image loading: For remote images, use an image-loading solution designed for asynchronous loading and caching.
  • Represent loading and empty states: Provide useful feedback while data is unavailable.
  • Choose stable state ownership: Keep important screen state outside individual list items when it must survive scrolling or item disposal.
  • Measure real performance: Evaluate scrolling in release builds as well as during development.
  • Do not assume every item remains composed: List items may be recomposed or disposed of as scrolling and data changes occur.

23. Common Errors and Troubleshooting

Problem Possible cause Solution
Items do not scroll The parent provides insufficient or incorrect constraints, or the list has no overflow Check the parent layout and ensure the lazy list receives usable space.
Nested scrolling throws a measurement error An unbounded LazyColumn is nested in another vertically scrollable container Use one parent LazyColumn or give the nested list an appropriate fixed or bounded height.
List item state appears on the wrong entry after reordering Items are identified only by position Provide stable, unique keys based on item identity.
Incorrect items appear after updating data The displayed collection or state update is incorrect Check the source of truth and update observable state correctly.
Spacing looks inconsistent Item padding and list arrangement are mixed without a clear plan Use consistent values for content padding, item padding, and arrangement.
List performance is poor Heavy item composition, repeated expensive calculations, or costly image work Simplify rows, use suitable keys and content types, and profile the release build.
items() cannot be resolved The collection extension import is missing Add import androidx.compose.foundation.lazy.items.
Programmatic scrolling does not compile A suspending scroll function is called outside a coroutine Call it from a coroutine scope, such as one created with rememberCoroutineScope().

24. Practical Exercise: Build a Task Manager List

Create a small task manager screen using LazyColumn. Each task should display a title and status. The screen should support a list of tasks, display an empty message when there are no tasks, and provide a button to add a new task.

Requirements

  • Create a Task data class containing a unique ID, title, and completion status.
  • Display tasks using LazyColumn.
  • Use a stable key for each task.
  • Create a reusable composable named TaskRow.
  • Show a helpful message when the list is empty.
  • Use consistent spacing and padding.
  • Add a button that inserts a new task into observable state.
  • Verify that the list scrolls correctly when many tasks are added.

Starter code

data class Task(
    val id: Int,
    val title: String,
    val isCompleted: Boolean
)

@Composable
fun TaskManagerScreen() {
    var tasks by remember {
        mutableStateOf(
            listOf(
                Task(1, "Learn LazyColumn", false),
                Task(2, "Practice LazyRow", true)
            )
        )
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Button(
            onClick = {
                val nextId = (tasks.maxOfOrNull { it.id } ?: 0) + 1
                tasks = tasks + Task(
                    id = nextId,
                    title = "New Task $nextId",
                    isCompleted = false
                )
            }
        ) {
            Text("Add Task")
        }

        Spacer(modifier = Modifier.height(12.dp))

        if (tasks.isEmpty()) {
            Text("No tasks yet. Add your first task.")
        } else {
            LazyColumn(
                modifier = Modifier.weight(1f),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(
                    items = tasks,
                    key = { task -> task.id }
                ) { task ->
                    TaskRow(task = task)
                }
            }
        }
    }
}

@Composable
fun TaskRow(task: Task) {
    Card(
        modifier = Modifier.fillMaxWidth()
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = task.title,
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = if (task.isCompleted) {
                    "Completed"
                } else {
                    "Pending"
                }
            )
        }
    }
}

Note: The example uses local Compose state to keep the exercise simple. A larger application would typically move task data and update logic into a ViewModel or another appropriate state holder.

25. Mini Project: Product Discovery Screen

Build a product discovery screen similar to the home screen of a shopping application.

Project features

  1. A title area displaying the name of the store.
  2. A horizontal LazyRow for product categories.
  3. A vertical LazyColumn for product entries.
  4. Reusable product cards containing a product name, price, and optional image.
  5. Stable keys for products and categories where necessary.
  6. A callback when the user selects a product.
  7. Loading, error, and empty states for data-driven implementations.
  8. Consistent spacing and responsive item widths.

Suggested folder organization

com.example.productapp
    data
        Product.kt
        ProductRepository.kt
    ui
        components
            ProductCard.kt
            CategoryChip.kt
        screens
            ProductScreen.kt
    viewmodel
        ProductViewModel.kt

This structure separates data access, reusable UI components, screens, and state management. For a small learning exercise, the same classes can initially be kept in fewer files and reorganized as the project grows.

26. Interview Questions and Answers

Q1. What is LazyColumn in Jetpack Compose?

Answer: LazyColumn is a vertically scrolling layout that composes and lays out items as they are needed for the visible viewport. It is suitable for displaying large or changing collections of data.

Q2. What is LazyRow?

Answer: LazyRow is a horizontally scrolling lazy layout. It is commonly used for categories, product carousels, image galleries, and featured content.

Q3. What is the difference between Column and LazyColumn?

Answer: A normal Column composes its children as part of its layout, while a LazyColumn creates and lays out collection entries on demand. For large scrolling collections, LazyColumn is generally the better choice.

Q4. What is the purpose of item()?

Answer: The item() function adds one entry to a lazy list. It is useful for headers, footers, banners, and other single pieces of content.

Q5. What is the purpose of items()?

Answer: The items() function creates list entries from a count or a collection. With a collection, its lambda receives each item so the UI can display that item's data.

Q6. What is itemsIndexed()?

Answer: It is an extension function that provides both the index and the data object for each collection entry. It is useful for numbered lists and index-dependent presentation.

Q7. Why should stable keys be used?

Answer: Stable, unique keys allow Compose to identify entries independently of their positions. This helps maintain the correct item identity and remembered state when entries are inserted, removed, or reordered.

Q8. What does contentPadding do?

Answer: It adds padding around the lazy list's content. It differs from padding applied to the outer list container or to each individual item.

Q9. How can a lazy list scroll to a specific entry?

Answer: Use the list state with scrollToItem() for immediate scrolling or animateScrollToItem() for animated scrolling. Since these are suspending functions, call them from a coroutine.

Q10. Can LazyColumn contain LazyRow?

Answer: Yes. A vertical list containing horizontal lists is a common layout pattern. Problems are more likely when unbounded scrollable containers are nested in the same direction without suitable constraints.

Q11. What is the purpose of contentType?

Answer: It identifies the general type of each list item. When a list contains different item structures, content types can help Compose reuse compatible item compositions.

Q12. How should an empty list be handled?

Answer: Check whether the collection is empty and display a useful empty-state message or action. In data-driven screens, distinguish empty results from loading and error states.

27. Quick Revision Notes

  • LazyColumn creates a vertical scrolling list.
  • LazyRow creates a horizontal scrolling list.
  • item() adds one entry to a lazy list.
  • items() renders entries from a count or collection.
  • itemsIndexed() provides both the index and the item.
  • key identifies an item using a stable, unique value.
  • contentType describes the general UI type of an entry.
  • contentPadding adds space around the list's content.
  • Arrangement.spacedBy() creates consistent gaps between entries.
  • rememberLazyListState() creates remembered scroll state.
  • animateScrollToItem() smoothly scrolls to a target index from a coroutine.
  • Prefer one parent lazy list when a screen has multiple vertically arranged sections.
  • Use stable keys and suitable item structures to support reliable updates and performance.

28. Conclusion

LazyColumn and LazyRow are essential Jetpack Compose components for building efficient, interactive, and maintainable lists. LazyColumn is ideal for vertically arranged collections, while LazyRow is ideal for horizontal collections such as categories and carousels.

To build production-ready lists, learn to use items(), itemsIndexed(), stable keys, content types, consistent spacing, reusable item composables, and list state. Handle empty and loading states thoughtfully, avoid incorrectly nested scrolling containers, and measure performance when the collection becomes large.

Practicing these techniques will help you create modern Android interfaces for contact directories, product catalogs, task managers, messaging screens, media applications, and many other data-driven experiences.

29. Additional Learning Resources

Previous Next
whatsapp